/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Nvigorate.Common;
using System.Linq;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;

namespace Nvigorate.ConsoleTools
{
	public class ArgumentParser
	{
		#region Fields

		private string _defaultConfigFileName = "args.yml";
		private Dictionary<string, Usage> _usageIndex = new Dictionary<string, Usage>();
		private Dictionary<string, string> _valueIndex = new Dictionary<string, string>();
		private List<string> _argumentNames = new List<string>();
		private Dictionary<string, List<string>> _exclusionGroups = new Dictionary<string, List<string>>();

		private List<string> _errors = new List<string>();
		#endregion

		#region Public Methods
		
		public bool Parse(string[] args)
		{
			//if empty args
			if(args.Length == 0 && File.Exists(_defaultConfigFileName))
			{
				return ParseFile(_defaultConfigFileName);
			}
			else if (args.Length == 1 && args[0][0]=='@')
			{
				string configName = args[0].Substring(1);
				if(!File.Exists(configName))
				{
                    System.Console.WriteLine("{0} not found. Invalid configuration file specified.", configName);
					return false;
				}
			}
            else if(args.Length == 1 && args[0] == @"/?")
            {
                System.Console.Write("\n\n{0}", this.ToString());
                return false;
            }
			else if (args.Length == 0)
			{
			    if(_usageIndex.Select(p => p.Value).ToList().Count(u => u.optional != true) > 0)
			    {
			        System.Console.Write("\n\n{0}", this.ToString());
			        return false;
			    }
			    return true;
			}

			ProcessArguments(PreProcess(args));

			try 
			{
				foreach (KeyValuePair<string, Usage> pair in _usageIndex)
				{
					ProcessValue(pair, Reflector.GetMemberType(this.GetType(), pair.Key));
				}
				
				if(_errors.Count > 0)
				{
					DisplayErrors();
					return false;
				}
			}
			catch(Exception ex)
			{
				System.Console.WriteLine(ex);
				System.Console.Write("\n\n{0}", this.ToString());
				return false;
			}
			
			return true;
		}

		public bool ParseFile(string filepath)
		{
			List<string> arguments = new List<string>();

			using (StreamReader reader = new StreamReader(filepath))
			{
				string configBody = reader.ReadToEnd();
				string[] lines = configBody.Split('\n');


				foreach (string line in lines)
				{
					string[] parts = line.Split(':');
					string tag = parts[0].Trim(':', ' ');
					string[] values = line.Substring(line.IndexOf(':') + 1).Trim().Split(' ');
					arguments.Add(tag);
					arguments.AddRange(values);
				}
			}

			ProcessArguments(PreProcess(arguments.ToArray()));

			try
			{
				foreach (KeyValuePair<string, Usage> pair in _usageIndex)
				{
					ProcessValue(pair, Reflector.GetMemberType(this.GetType(), pair.Key));
				}
				if (_errors.Count > 0)
				{
					DisplayErrors();
					return false;
				}
			}
			catch (Exception ex)
			{
				System.Console.WriteLine(ex.Message.ToString());
				System.Console.Write("\n\n{0}", this.ToString());
				return false;
			}

			return true;
		}

		#endregion

		#region Private Methods

		private void BuildPropertyIndex()
		{
            List<string> properties = Reflector.GetPropertyInfoByAttribute<Usage>(GetType()).Select(p => p.Name).ToList();
			foreach (string property in properties)
			{
			    Usage use = Reflector.GetPropertyAttributes<Usage>(GetType(), property).First();
				_usageIndex.Add(property, use);
				_argumentNames.Add((use.name == "") ? property : use.name);
			}
		}

		private void DisplayErrors()
		{
			System.Console.Write("\r\n");
			foreach(string error in _errors)
			{
				System.Console.WriteLine("{0}\n",error);
			}
		}

		private bool IsLegalValue(object value, string legalValues)
		{
			if (legalValues == "") return true;
			List<string> legalList = new List<string>(ScrubList(legalValues));
			return legalList.Contains(value.ToString());
		}
		
		private bool IsInRange(IComparable value, string ranges, Type expected)
		{
			if(Reflector.IsCollection(expected)) expected = expected.GetGenericArguments()[0];
			if (ranges == "") return true;
			foreach (string range in ranges.Split(','))
			{
				IComparable upperBound = range.Split('-')[1];
				IComparable lowerBound = range.Split('-')[0];

				if(Reflector.TypeCastCompare(value, lowerBound, expected) >= 0 &&
					Reflector.TypeCastCompare(value, upperBound, expected) <= 0) return true;
			}

			return false;
		}

		private string[] PreProcess(string[] args)
		{
			List<string> arguments = new List<string>();

			foreach (string argument in args)
			{
				if((argument.Contains(":") && !argument.Contains("http")) ||
					argument.Contains("http") && argument.IndexOf(':') != argument.LastIndexOf(':'))
				{
					string[] parts = argument.Split(':');
					
					if(parts.Length > 1)
					{
						if(parts[0][0] == '/')
						{
							arguments.Add(parts[0]);
							arguments.Add(argument.Substring(argument.IndexOf(':') + 1));
						}
						else
						{
							arguments.Add(argument);
						}
					}
				}
				else
				{
					arguments.Add(argument);
				}
			}

			return arguments.ToArray();
		}

		private void ProcessArguments(string[] args)
		{
			List<string> arguments = new List<string>(args);
			List<int> argIndices = new List<int>();
			Dictionary<int, string> argByIndex = new Dictionary<int, string>();

			//Build indices in the original array at which actual arguments occur
			foreach (string argument in arguments)
			{
				if (_argumentNames.Contains(argument.Trim('/', ':', '!').Trim()))
				{
					int index = arguments.IndexOf(argument);
					argIndices.Add(index);
					argByIndex.Add(index, argument.Trim('/', ':', '!').Trim());
				}
			}

			//Traverse the list of arguments and build a dictionary where each argument
			//is followed by the intended value(s) which occur between it and the next
			for (int i = 0; i < argIndices.Count; i++)
			{
				DelimitedBuilder builder = new DelimitedBuilder(" ");
				if (i == argIndices.Count - 1)
				{
					if (argIndices[i] == arguments.Count-1)
					{
						builder.Append(arguments[argIndices[i]].Contains("/"));
					}
					
					for (int index = argIndices[i] + 1; index < arguments.Count; index++)
					{
						builder.Append(arguments[index], ((index < arguments.Count - 1) && !arguments[index].Contains(",")));
					}
				}
				else
				{
					int nextIndex = argIndices[i + 1];
					
					if (nextIndex == argIndices[i] + 1)
					{
						builder.Append(arguments[argIndices[i]].Contains("/"));
					}
					
					for (int index = argIndices[i] + 1; index < nextIndex; index++)
					{
						builder.Append(arguments[index], index < nextIndex - 1);
					}
				}
				_valueIndex.Add(arguments[argIndices[i]].Trim('/', ':', '!').Trim(), builder.ToString());
			}

		}
		
		private void ProcessValue(KeyValuePair<string, Usage> pair, Type expected)
		{
			Usage use = pair.Value;
			if(use.name == "") use.name = pair.Key;
			if(!use.optional && !_valueIndex.ContainsKey(use.name))
			{
				foreach (string alternate in use.alternates.Split(' '))
				{
					if(_valueIndex.ContainsKey(alternate)) return;
				}
				
				_errors.Add(use.GetErrorMessage(ArgumentError.Missing, ""));
				return;
			}
			else if(!_valueIndex.ContainsKey(use.name))
			{
				return;
			}
			
			string rawValue = _valueIndex[use.name];
			
			if(Reflector.IsDictionary(expected))
			{
			    var dictionary = (IDictionary) Reflector.Read(this, pair.Key);

                foreach (string value in rawValue.Split(','))
				{
					if (!IsLegalValue(value, use.legalValues))
						_errors.Add(use.GetErrorMessage(ArgumentError.IllegalValue, rawValue));
					if (!IsInRange(value, use.ranges, expected))
						_errors.Add(use.GetErrorMessage(ArgumentError.OutofRange, rawValue));
						
					dictionary.Add(value.Split('|')[0], value.Split('|')[1]);
				}
			}
			else if(expected.IsGenericType && Reflector.IsCollection(expected))
			{
				foreach (string value in rawValue.Split(','))
				{
					if(!IsLegalValue(value, use.legalValues))
						_errors.Add(use.GetErrorMessage(ArgumentError.IllegalValue, rawValue));
					if (!IsInRange(value, use.ranges, expected))
						_errors.Add(use.GetErrorMessage(ArgumentError.OutofRange, rawValue));
						
					Reflector.Write(this, pair.Key, value);
				}
			}
			else if(Reflector.IsCollection(expected))
			{
				foreach (string value in rawValue.Split(','))
				{
					if (!IsLegalValue(value, use.legalValues))
						_errors.Add(use.GetErrorMessage(ArgumentError.IllegalValue, rawValue));
					if (!IsInRange(value, use.ranges, expected))
						_errors.Add(use.GetErrorMessage(ArgumentError.OutofRange, rawValue));

					Reflector.Write(this, pair.Key, new ArrayList(ScrubList(rawValue)));
				}
			}
			else
			{
				if (!IsLegalValue(rawValue, use.legalValues))
					_errors.Add(use.GetErrorMessage(ArgumentError.IllegalValue, rawValue));
				if (!IsInRange(rawValue, use.ranges, expected))
					_errors.Add(use.GetErrorMessage(ArgumentError.OutofRange, rawValue));
					
				Reflector.Write(this, pair.Key, rawValue);
			}
		}
		
		private string[] ScrubList(string value)
		{
			List<string> items = new List<string>();
			foreach (string item in value.Split(','))
			{
				items.Add(item.Trim());
			}
			return items.ToArray();
		}

		#endregion

		#region Overloaded Methods
		
		public override string ToString()
		{
		    var propertyAttributes = Reflector.GetPropertyAttributes<Usage>(GetType());
		    return DelimitedBuilder.Construct(
                propertyAttributes.Select(p => p.Value2.Select(a => a.ToString())).Flatten(),
                "\n");
		}
		
		#endregion

		#region Constructor
		
		public ArgumentParser()
		{
			BuildPropertyIndex();
		}
		
		#endregion
	}
}
